| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- <template>
- <div class="admin--page-content">
- <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
- <div v-else class="admin--form">
- <form @submit.prevent="handleSubmit">
- <table class="admin--form--table">
- <colgroup>
- <col style="width: 120px;">
- <col>
- </colgroup>
- <tbody>
- <tr>
- <th><div>아이템명 <span class="admin--required">*</span></div></th>
- <td>
- <div class="input--wrap">
- <input v-model="formData.name" type="text" class="admin--form-input" placeholder="예: 등급 포인트 +100" required />
- </div>
- </td>
- </tr>
- <tr>
- <th><div>구분 <span class="admin--required">*</span></div></th>
- <td>
- <div class="input--wrap">
- <label class="admin--radio-label">
- <input type="radio" v-model="formData.type" value="T" /> 진출권
- </label>
- <label class="admin--radio-label ml--16">
- <input type="radio" v-model="formData.type" value="P" /> 포인트
- </label>
- <label class="admin--radio-label ml--16">
- <input type="radio" v-model="formData.type" value="B" /> 뱃지
- </label>
- </div>
- </td>
- </tr>
- <tr v-if="formData.type !== 'T'">
- <th>
- <div>
- 포인트 <span v-if="formData.type === 'P'" class="admin--required">*</span>
- </div>
- </th>
- <td>
- <div class="input--wrap">
- <input v-model.number="formData.point" type="number" min="0" class="admin--form-input w--200" placeholder="예: 100" />
- </div>
- <p class="mt--10" v-if="formData.type === 'P'">아이템 구분이 "포인트"인 경우 필수 입력</p>
- <p class="mt--10" v-else>아이템 구분이 "뱃지"인 경우 선택 입력</p>
- </td>
- </tr>
- <tr>
- <th><div>이미지</div></th>
- <td>
- <div class="input--wrap">
- <input
- ref="imageInput"
- type="file"
- accept="image/*"
- class="admin--form-file-hidden"
- @change="onImageChange"
- />
- <button type="button" class="admin--btn-small admin--btn-blue" @click="triggerImageInput">
- 이미지 선택
- </button>
- <span v-if="newImage" class="ml--16">{{ newImage.file.name }} (새 이미지)</span>
- </div>
- <p class="mt--10">JPG/PNG/GIF/WebP, 10MB 이하. 새 이미지 선택 시 기존 이미지가 교체됩니다.</p>
- <!-- 새 이미지 미리보기 (선택 시) -->
- <div v-if="newImage" class="onboard--photo-item mt--10">
- <img :src="newImage.preview" alt="새 이미지 미리보기" />
- <button type="button" class="onboard--photo-remove" @click="cancelNewImage"></button>
- <span class="onboard--photo-new">NEW</span>
- </div>
- <!-- 기존 이미지 (새 이미지 선택 안 했을 때만) -->
- <div v-else-if="existingImagePath" class="onboard--photo-item mt--10">
- <img :src="getImageUrl(existingImagePath)" alt="기존 이미지" />
- <button type="button" class="onboard--photo-remove" @click="removeExistingImage"></button>
- </div>
- </td>
- </tr>
- <tr>
- <th><div>상태</div></th>
- <td>
- <div class="input--wrap">
- <label class="admin--radio-label">
- <input type="radio" v-model="formData.status_YN" value="Y" /> 사용중
- </label>
- <label class="admin--radio-label ml--16">
- <input type="radio" v-model="formData.status_YN" value="N" /> 미사용
- </label>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- <!-- 버튼 영역 -->
- <div class="admin--form-actions">
- <button type="button" class="admin--btn" @click="goToDetail">
- ← 취소
- </button>
- <button type="submit" class="admin--btn admin--btn-red ml--auto" :disabled="isSaving">
- {{ isSaving ? "저장 중..." : "저장" }}
- </button>
- </div>
- <!-- 성공/에러 메시지 -->
- <div v-if="successMessage" class="admin--alert admin--alert-success">
- {{ successMessage }}
- </div>
- <div v-if="errorMessage" class="admin--alert admin--alert-error">
- {{ errorMessage }}
- </div>
- </form>
- </div>
- <!-- 알림 모달 -->
- <AdminAlertModal
- v-if="alertModal.show"
- :title="alertModal.title"
- :message="alertModal.message"
- :type="alertModal.type"
- @confirm="handleAlertConfirm"
- @cancel="handleAlertCancel"
- @close="closeAlertModal"
- />
- </div>
- </template>
- <script setup>
- import { ref, watch, onMounted } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
- definePageMeta({
- layout: "admin",
- middleware: ["auth"],
- });
- const route = useRoute();
- const router = useRouter();
- const { get, put, upload, del } = useApi();
- const { getImageUrl } = useImage();
- const itemId = route.params.id;
- const isLoading = ref(true);
- const isSaving = ref(false);
- const successMessage = ref("");
- const errorMessage = ref("");
- const formData = ref({
- name: "",
- type: "T",
- point: null,
- status_YN: "Y",
- });
- // 이미지 상태
- const imageInput = ref(null);
- const existingImagePath = ref(""); // 기존 image_path
- const removedExisting = ref(false); // 기존 이미지 제거 의도
- const newImage = ref(null); // { file, preview }
- const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
- const triggerImageInput = () => imageInput.value?.click();
- const onImageChange = (e) => {
- const file = (e.target.files || [])[0];
- e.target.value = "";
- if (!file) return;
- if (!file.type.startsWith("image/")) {
- errorMessage.value = "이미지 파일만 업로드할 수 있습니다.";
- return;
- }
- if (file.size > MAX_IMAGE_SIZE) {
- errorMessage.value = "이미지가 10MB를 초과합니다.";
- return;
- }
- if (newImage.value) URL.revokeObjectURL(newImage.value.preview);
- newImage.value = { file, preview: URL.createObjectURL(file) };
- };
- const cancelNewImage = () => {
- if (newImage.value) {
- URL.revokeObjectURL(newImage.value.preview);
- newImage.value = null;
- }
- };
- const removeExistingImage = () => {
- removedExisting.value = true;
- existingImagePath.value = "";
- };
- // 진출권으로 바뀌면 포인트 초기화
- watch(
- () => formData.value.type,
- (val) => {
- if (val === "T") formData.value.point = null;
- }
- );
- // 알림 모달
- const alertModal = ref({ show: false, title: "알림", message: "", type: "alert", onConfirm: null });
- const showAlert = (message, title = "알림") => {
- alertModal.value = { show: true, title, message, type: "alert", onConfirm: null };
- };
- const closeAlertModal = () => { alertModal.value.show = false; };
- const handleAlertConfirm = () => {
- if (alertModal.value.onConfirm) alertModal.value.onConfirm();
- closeAlertModal();
- };
- const handleAlertCancel = () => closeAlertModal();
- // 기존 데이터 로드
- const loadDetail = async () => {
- const { data, error } = await get(`/item/${itemId}`);
- if (error || !data?.success) {
- errorMessage.value = error?.message || data?.message || "조회에 실패했습니다.";
- isLoading.value = false;
- return;
- }
- const row = data.data || {};
- formData.value = {
- name: row.name ?? "",
- type: row.type ?? "T",
- point: row.point ?? null,
- status_YN: row.status_YN ?? "Y",
- };
- existingImagePath.value = row.file_path ?? "";
- removedExisting.value = false;
- newImage.value = null;
- isLoading.value = false;
- };
- // 폼 제출
- const handleSubmit = async () => {
- successMessage.value = "";
- errorMessage.value = "";
- const name = formData.value.name.trim();
- if (!name) return (errorMessage.value = "아이템명을 입력하세요.");
- if (name.length > 50) return (errorMessage.value = "아이템명은 50자 이내로 입력하세요.");
- if (!["T", "P", "B"].includes(formData.value.type)) {
- return (errorMessage.value = "구분을 선택하세요.");
- }
- if (formData.value.type === "P") {
- if (formData.value.point === null || formData.value.point === "" || Number(formData.value.point) < 0) {
- return (errorMessage.value = "포인트를 입력하세요.");
- }
- } else if (formData.value.type === "B") {
- if (formData.value.point !== null && formData.value.point !== "" && Number(formData.value.point) < 0) {
- return (errorMessage.value = "포인트는 0 이상의 숫자여야 합니다.");
- }
- }
- isSaving.value = true;
- try {
- // 1) 텍스트 필드 수정 (PUT)
- const payload = {
- name,
- type: formData.value.type,
- status_YN: formData.value.status_YN,
- };
- if (formData.value.type !== "T" && formData.value.point !== null && formData.value.point !== "") {
- payload.point = Number(formData.value.point);
- }
- const { data, error } = await put(`/item/${itemId}`, payload);
- if (error || !data?.success) {
- errorMessage.value = error?.message || data?.message || "수정에 실패했습니다.";
- return;
- }
- // 2) 이미지 처리 — 새 이미지 우선, 그 다음 제거 의도
- if (newImage.value) {
- const fd = new FormData();
- fd.append("image", newImage.value.file);
- const { data: imgRes, error: imgErr } = await upload(`/item/${itemId}/image`, fd);
- if (imgErr || !imgRes?.success) {
- errorMessage.value = "아이템은 수정됐지만 이미지 교체에 실패했습니다.";
- setTimeout(() => router.push(`/site-manager/item/detail/${itemId}`), 1500);
- return;
- }
- } else if (removedExisting.value) {
- const { data: rmRes, error: rmErr } = await del(`/item/${itemId}/image`);
- if (rmErr || !rmRes?.success) {
- errorMessage.value = "아이템은 수정됐지만 이미지 제거에 실패했습니다.";
- setTimeout(() => router.push(`/site-manager/item/detail/${itemId}`), 1500);
- return;
- }
- }
- successMessage.value = data.message || "수정되었습니다.";
- setTimeout(() => {
- router.push(`/site-manager/item/detail/${itemId}`);
- }, 1000);
- } catch (e) {
- errorMessage.value = "서버 오류가 발생했습니다.";
- console.error("Update error:", e);
- } finally {
- isSaving.value = false;
- }
- };
- const goToDetail = () => router.push(`/site-manager/item/detail/${itemId}`);
- onMounted(() => {
- loadDetail();
- });
- </script>
|